/* p5.play by Paolo Pedercini/molleindustria, 2015 http://molleindustria.org/ */ (function(root, factory) { if (typeof define === 'function' && define.amd) define('p5.play', ['p5'], function(p5) { (factory(p5)); }); else if (typeof exports === 'object') factory(require('../p5')); else factory(root.p5); }(this, function(p5) { /** * p5.play is a library for p5.js to facilitate the creation of games and gamelike * projects. * * It provides a flexible Sprite class to manage visual objects in 2D space * and features such as animation support, basic collision detection * and resolution, mouse and keyboard interactions, and a virtual camera. * * p5.play is not a box2D-derived physics engine, it doesn't use events, and it's * designed to be understood and possibly modified by intermediate programmers. * * See the examples folder for more info on how to use this library. * * @module p5.play * @submodule p5.play * @for p5.play * @main */ // ============================================================================= // initialization // ============================================================================= // This is the new way to initialize custom p5 properties for any p5 instance. // The goal is to migrate lazy P5 properties over to this method. // @see https://github.com/molleindustria/p5.play/issues/46 p5.prototype.registerMethod('init', function p5PlayInit() { /** * The sketch camera automatically created at the beginning of a sketch. * A camera facilitates scrolling and zooming for scenes extending beyond * the canvas. A camera has a position, a zoom factor, and the mouse * coordinates relative to the view. * * In p5.js terms the camera wraps the whole drawing cycle in a * transformation matrix but it can be disabled anytime during the draw * cycle, for example to draw interface elements in an absolute position. * * @property camera * @type {camera} */ this.camera = new Camera(this, 0, 0, 1); this.camera.init = false; }); // This provides a way for us to lazily define properties that // are global to p5 instances. // // Note that this isn't just an optimization: p5 currently provides no // way for add-ons to be notified when new p5 instances are created, so // lazily creating these properties is the *only* mechanism available // to us. For more information, see: // // https://github.com/processing/p5.js/issues/1263 function defineLazyP5Property(name, getter) { Object.defineProperty(p5.prototype, name, { configurable: true, enumerable: true, get: function() { var context = (this instanceof p5 && !this._isGlobal) ? this : window; if (typeof(context._p5PlayProperties) === 'undefined') { context._p5PlayProperties = {}; } if (!(name in context._p5PlayProperties)) { context._p5PlayProperties[name] = getter.call(context); } return context._p5PlayProperties[name]; } }); } // This returns a factory function, suitable for passing to // defineLazyP5Property, that returns a subclass of the given // constructor that is always bound to a particular p5 instance. function boundConstructorFactory(constructor) { if (typeof(constructor) !== 'function') throw new Error('constructor must be a function'); return function createBoundConstructor() { var pInst = this; function F() { var args = Array.prototype.slice.call(arguments); return constructor.apply(this, [pInst].concat(args)); } F.prototype = constructor.prototype; return F; }; } // This is a utility that makes it easy to define convenient aliases to // pre-bound p5 instance methods. // // For example: // // var pInstBind = createPInstBinder(pInst); // // var createVector = pInstBind('createVector'); // var loadImage = pInstBind('loadImage'); // // The above will create functions createVector and loadImage, which can be // used similar to p5 global mode--however, they're bound to specific p5 // instances, and can thus be used outside of global mode. function createPInstBinder(pInst) { return function pInstBind(methodName) { var method = pInst[methodName]; if (typeof(method) !== 'function') throw new Error('"' + methodName + '" is not a p5 method'); return method.bind(pInst); }; } // These are utility p5 functions that don't depend on p5 instance state in // order to work properly, so we'll go ahead and make them easy to // access without needing to bind them to a p5 instance. var abs = p5.prototype.abs; var radians = p5.prototype.radians; var dist = p5.prototype.dist; var degrees = p5.prototype.degrees; var pow = p5.prototype.pow; var round = p5.prototype.round; // ============================================================================= // p5 additions // ============================================================================= /** * A Group containing all the sprites in the sketch. * * @property allSprites * @type {Group} */ defineLazyP5Property('allSprites', function() { return new p5.prototype.Group(); }); p5.prototype.spriteUpdate = true; /** * A Sprite is the main building block of p5.play: * an element able to store images or animations with a set of * properties such as position and visibility. * A Sprite can have a collider that defines the active area to detect * collisions or overlappings with other sprites and mouse interactions. * * Sprites created using createSprite (the preferred way) are added to the * allSprites group and given a depth value that puts it in front of all * other sprites. * * @method createSprite * @param {Number} x Initial x coordinate * @param {Number} y Initial y coordinate * @param {Number} width Width of the placeholder rectangle and of the * collider until an image or new collider are set * @param {Number} height Height of the placeholder rectangle and of the * collider until an image or new collider are set * @return {Object} The new sprite instance */ p5.prototype.createSprite = function(x, y, width, height) { var s = new Sprite(this, x, y, width, height); s.depth = this.allSprites.maxDepth()+1; this.allSprites.add(s); return s; }; /** * Removes a Sprite from the sketch. * The removed Sprite won't be drawn or updated anymore. * Equivalent to Sprite.remove() * * @method removeSprite * @param {Object} sprite Sprite to be removed */ p5.prototype.removeSprite = function(sprite) { sprite.remove(); }; /** * Updates all the sprites in the sketch (position, animation...) * it's called automatically at every draw(). * It can be paused by passing a parameter true or false; * Note: it does not render the sprites. * * @method updateSprites * @param {Boolean} updating false to pause the update, true to resume */ p5.prototype.updateSprites = function(upd) { if(upd === false) this.spriteUpdate = false; if(upd === true) this.spriteUpdate = true; if(this.spriteUpdate) for(var i = 0; i 1 hyper elastic /** * Coefficient of restitution. The velocity lost after bouncing. * 1: perfectly elastic, no energy is lost * 0: perfectly inelastic, no bouncing * less than 1: inelastic, this is the most common in nature * greater than 1: hyper elastic, energy is increased like in a pinball bumper * * @property restitution * @type {Number} * @default 1 */ this.restitution = 1; /** * Rotation in degrees of the visual element (image or animation) * Note: this is not the movement's direction, see getDirection. * * @property rotation * @type {Number} * @default 0 */ Object.defineProperty(this, 'rotation', { enumerable: true, get: function() { return this._rotation; }, set: function(value) { this._rotation = value; if (this.rotateToDirection) { this.setSpeed(this.getSpeed(), value); } } }); /** * Internal rotation variable (expressed in degrees). * Note: external callers access this through the rotation property above. * * @private * @property _rotation * @type {Number} * @default 0 */ this._rotation = 0; /** * Rotation change in degrees per frame of thevisual element (image or animation) * Note: this is not the movement's direction, see getDirection. * * @property rotationSpeed * @type {Number} * @default 0 */ this.rotationSpeed = 0; /** * Automatically lock the rotation property of the visual element * (image or animation) to the sprite's movement direction and vice versa. * * @property rotateToDirection * @type {Boolean} * @default false */ this.rotateToDirection = false; /** * Determines the rendering order within a group: a sprite with * lower depth will appear below the ones with higher depth. * * Note: drawing a group before another with drawSprites will make * its members appear below the second one, like in normal p5 canvas * drawing. * * @property depth * @type {Number} * @default One more than the greatest existing sprite depth, when calling * createSprite(). When calling new Sprite() directly, depth will * initialize to 0 (not recommended). */ this.depth = 0; /** * Determines the sprite's scale. * Example: 2 will be twice the native size of the visuals, * 0.5 will be half. Scaling up may make images blurry. * * @property scale * @type {Number} * @default 1 */ this.scale = 1; var dirX = 1; var dirY = 1; /** * The sprite's visibility. * * @property visible * @type {Boolean} * @default true */ this.visible = true; /** * If set to true sprite will track its mouse state. * the properties mouseIsPressed and mouseIsOver will be updated. * Note: automatically set to true if the functions * onMouseReleased or onMousePressed are set. * * @property mouseActive * @type {Boolean} * @default false */ this.mouseActive = false; /** * True if mouse is on the sprite's collider. * Read only. * * @property mouseIsOver * @type {Boolean} */ this.mouseIsOver = false; /** * True if mouse is pressed on the sprite's collider. * Read only. * * @property mouseIsPressed * @type {Boolean} */ this.mouseIsPressed = false; /* * Width of the sprite's current image. * If no images or animations are set it's the width of the * placeholder rectangle. * Used internally to make calculations and draw the sprite. * * @private * @property _internalWidth * @type {Number} * @default 100 */ this._internalWidth = _w; /* * Height of the sprite's current image. * If no images or animations are set it's the height of the * placeholder rectangle. * Used internally to make calculations and draw the sprite. * * @private * @property _internalHeight * @type {Number} * @default 100 */ this._internalHeight = _h; /* * _internalWidth and _internalHeight are used for all p5.play * calculations, but width and height can be extended. For example, * you may want users to always get and set a scaled width: Object.defineProperty(this, 'width', { enumerable: true, configurable: true, get: function() { return this._internalWidth * this.scale; }, set: function(value) { this._internalWidth = value / this.scale; } }); */ /** * Width of the sprite's current image. * If no images or animations are set it's the width of the * placeholder rectangle. * * @property width * @type {Number} * @default 100 */ Object.defineProperty(this, 'width', { enumerable: true, configurable: true, get: function() { return this._internalWidth; }, set: function(value) { this._internalWidth = value; } }); if(_w === undefined) this.width = 100; else this.width = _w; /** * Height of the sprite's current image. * If no images or animations are set it's the height of the * placeholder rectangle. * * @property height * @type {Number} * @default 100 */ Object.defineProperty(this, 'height', { enumerable: true, configurable: true, get: function() { return this._internalHeight; }, set: function(value) { this._internalHeight = value; } }); if(_h === undefined) this.height = 100; else this.height = _h; /** * Unscaled width of the sprite * If no images or animations are set it's the width of the * placeholder rectangle. * * @property originalWidth * @type {Number} * @default 100 */ this.originalWidth = this._internalWidth; /** * Unscaled height of the sprite * If no images or animations are set it's the height of the * placeholder rectangle. * * @property originalHeight * @type {Number} * @default 100 */ this.originalHeight = this._internalHeight; /** * True if the sprite has been removed. * * @property removed * @type {Boolean} */ this.removed = false; /** * Cycles before self removal. * Set it to initiate a countdown, every draw cycle the property is * reduced by 1 unit. At 0 it will call a sprite.remove() * Disabled if set to -1. * * @property life * @type {Number} * @default -1 */ this.life = -1; /** * If set to true, draws an outline of the collider, the depth, and center. * * @property debug * @type {Boolean} * @default false */ this.debug = false; /** * If no image or animations are set this is color of the * placeholder rectangle * * @property shapeColor * @type {color} */ this.shapeColor = color(random(255), random(255), random(255)); /** * Groups the sprite belongs to, including allSprites * * @property groups * @type {Array} */ this.groups = []; var animations = {}; //The current animation's label. var currentAnimation = ''; /** * Reference to the current animation. * * @property animation * @type {Animation} */ this.animation = undefined; /* * @private * Keep animation properties in sync with how the animation changes. */ this._syncAnimationSizes = function() { //has an animation but the collider is still default //the animation wasn't loaded. if the animation is not a 1x1 image //it means it just finished loading if(this.colliderType === 'default' && animations[currentAnimation].getWidth() !== 1 && animations[currentAnimation].getHeight() !== 1) { this.collider = this.getBoundingBox(); this.colliderType = 'image'; this._internalWidth = animations[currentAnimation].getWidth()*abs(this._getScaleX()); this._internalHeight = animations[currentAnimation].getHeight()*abs(this._getScaleY()); //quadTree.insert(this); } //update size and collider if(animations[currentAnimation].frameChanged || this.width === undefined || this.height === undefined) { //this.collider = this.getBoundingBox(); this._internalWidth = animations[currentAnimation].getWidth()*abs(this._getScaleX()); this._internalHeight = animations[currentAnimation].getHeight()*abs(this._getScaleY()); } }; /** * Updates the sprite. * Called automatically at the beginning of the draw cycle. * * @method update */ this.update = function() { if(!this.removed) { //if there has been a change somewhere after the last update //the old position is the last position registered in the update if(this.newPosition !== this.position) this.previousPosition = createVector(this.newPosition.x, this.newPosition.y); else this.previousPosition = createVector(this.position.x, this.position.y); this.velocity.x *= 1 - this.friction; this.velocity.y *= 1 - this.friction; if(this.maxSpeed !== -1) this.limitSpeed(this.maxSpeed); if(this.rotateToDirection && this.velocity.mag() > 0) this._rotation = this.getDirection(); this.rotation += this.rotationSpeed; this.position.x += this.velocity.x; this.position.y += this.velocity.y; this.newPosition = createVector(this.position.x, this.position.y); this.deltaX = this.position.x - this.previousPosition.x; this.deltaY = this.position.y - this.previousPosition.y; //if there is an animation if(animations[currentAnimation]) { //update it animations[currentAnimation].update(); this._syncAnimationSizes(); } //a collider is created either manually with setCollider or //when I check this sprite for collisions or overlaps if(this.collider) { if(this.collider instanceof AABB) { //scale / rotate collider var t; if (pInst._angleMode === pInst.RADIANS) { t = radians(this.rotation); } else { t = this.rotation; } if(this.colliderType === 'custom') { this.collider.extents.x = this.collider.originalExtents.x * abs(this._getScaleX()) * abs(cos(t)) + this.collider.originalExtents.y * abs(this._getScaleY()) * abs(sin(t)); this.collider.extents.y = this.collider.originalExtents.x * abs(this._getScaleX()) * abs(sin(t)) + this.collider.originalExtents.y * abs(this._getScaleY()) * abs(cos(t)); } else if(this.colliderType === 'default') { this.collider.extents.x = this._internalWidth * abs(this._getScaleX()) * abs(cos(t)) + this._internalHeight * abs(this._getScaleY()) * abs(sin(t)); this.collider.extents.y = this._internalWidth * abs(this._getScaleX()) * abs(sin(t)) + this._internalHeight * abs(this._getScaleY()) * abs(cos(t)); } else if(this.colliderType === 'image') { this.collider.extents.x = this._internalWidth * abs(cos(t)) + this._internalHeight * abs(sin(t)); this.collider.extents.y = this._internalWidth * abs(sin(t)) + this._internalHeight * abs(cos(t)); } } if(this.collider instanceof CircleCollider) { //print(this.scale); this.collider.radius = this.collider.originalRadius * abs(this.scale); } }//end collider != null //mouse actions if (this.mouseActive) { //if no collider set it if(!this.collider) this.setDefaultCollider(); this.mouseUpdate(); } else { if (typeof(this.onMouseOver) === 'function' || typeof(this.onMouseOut) === 'function' || typeof(this.onMousePressed) === 'function' || typeof(this.onMouseReleased) === 'function') { //if a mouse function is set //it's implied we want to have it mouse active so //we do this automatically this.mouseActive = true; //if no collider set it if(!this.collider) this.setDefaultCollider(); this.mouseUpdate(); } } //self destruction countdown if (this.life>0) this.life--; if (this.life === 0) this.remove(); } };//end update /** * Creates a default collider matching the size of the * placeholder rectangle or the bounding box of the image. * * @method setDefaultCollider */ this.setDefaultCollider = function() { //if has animation get the animation bounding box //working only for preloaded images if(animations[currentAnimation] && (animations[currentAnimation].getWidth() !== 1 && animations[currentAnimation].getHeight() !== 1)) { this.collider = this.getBoundingBox(); this._internalWidth = animations[currentAnimation].getWidth()*abs(this._getScaleX()); this._internalHeight = animations[currentAnimation].getHeight()*abs(this._getScaleY()); //quadTree.insert(this); this.colliderType = 'image'; //print("IMAGE COLLIDER ADDED"); } else if(animations[currentAnimation] && animations[currentAnimation].getWidth() === 1 && animations[currentAnimation].getHeight() === 1) { //animation is still loading //print("wait"); } else //get the with and height defined at the creation { this.collider = new AABB(pInst, this.position, createVector(this._internalWidth, this._internalHeight)); //quadTree.insert(this); this.colliderType = 'default'; } pInst.quadTree.insert(this); }; /** * Updates the sprite mouse states and triggers the mouse events: * onMouseOver, onMouseOut, onMousePressed, onMouseReleased * * @method mouseUpdate */ this.mouseUpdate = function() { var mouseWasOver = this.mouseIsOver; var mouseWasPressed = this.mouseIsPressed; this.mouseIsOver = false; this.mouseIsPressed = false; var mousePosition; if(camera.active) mousePosition = createVector(camera.mouseX, camera.mouseY); else mousePosition = createVector(pInst.mouseX, pInst.mouseY); //rollover if(this.collider) { if (this.collider instanceof CircleCollider) { if (dist(mousePosition.x, mousePosition.y, this.collider.center.x, this.collider.center.y) < this.collider.radius) this.mouseIsOver = true; } else if (this.collider instanceof AABB) { if (mousePosition.x > this.collider.left() && mousePosition.y > this.collider.top() && mousePosition.x < this.collider.right() && mousePosition.y < this.collider.bottom()) { this.mouseIsOver = true; } } //global p5 var if(this.mouseIsOver && pInst.mouseIsPressed) this.mouseIsPressed = true; //event change - call functions if(!mouseWasOver && this.mouseIsOver && this.onMouseOver !== undefined) if(typeof(this.onMouseOver) === 'function') this.onMouseOver.call(this, this); else print('Warning: onMouseOver should be a function'); if(mouseWasOver && !this.mouseIsOver && this.onMouseOut !== undefined) if(typeof(this.onMouseOut) === 'function') this.onMouseOut.call(this, this); else print('Warning: onMouseOut should be a function'); if(!mouseWasPressed && this.mouseIsPressed && this.onMousePressed !== undefined) if(typeof(this.onMousePressed) === 'function') this.onMousePressed.call(this, this); else print('Warning: onMousePressed should be a function'); if(mouseWasPressed && !pInst.mouseIsPressed && !this.mouseIsPressed && this.onMouseReleased !== undefined) if(typeof(this.onMouseReleased) === 'function') this.onMouseReleased.call(this, this); else print('Warning: onMouseReleased should be a function'); } }; /** * Sets a collider for the sprite. * * In p5.play a Collider is an invisible circle or rectangle * that can have any size or position relative to the sprite and which * will be used to detect collisions and overlapping with other sprites, * or the mouse cursor. * * If the sprite is checked for collision, bounce, overlapping or mouse events * a collider is automatically created from the width and height parameter * passed at the creation of the sprite or the from the image dimension in case * of animated sprites. * * Often the image bounding box is not appropriate as the active area for * collision detection so you can set a circular or rectangular sprite with * different dimensions and offset from the sprite's center. * * There are four ways to call this method: * * 1. setCollider("rectangle") * 2. setCollider("rectangle", offsetX, offsetY, width, height) * 3. setCollider("circle") * 4. setCollider("circle", offsetX, offsetY, radius) * * @method setCollider * @param {String} type Either "rectangle" or "circle" * @param {Number} offsetX Collider x position from the center of the sprite * @param {Number} offsetY Collider y position from the center of the sprite * @param {Number} width Collider width or radius * @param {Number} height Collider height * @throws {TypeError} if given invalid parameters. */ this.setCollider = function(type, offsetX, offsetY, width, height) { if (!(type === 'rectangle' || type === 'circle')) { throw new TypeError('setCollider expects the first argument to be either "circle" or "rectangle"'); } else if (type === 'circle' && arguments.length > 1 && arguments.length < 4) { throw new TypeError('Usage: setCollider("circle") or setCollider("circle", offsetX, offsetY, radius)'); } else if (type === 'circle' && arguments.length > 4) { pInst._warn('Extra parameters to setCollider were ignored. Usage: setCollider("circle") or setCollider("circle", offsetX, offsetY, radius)'); } else if (type === 'rectangle' && arguments.length > 1 && arguments.length < 5) { throw new TypeError('Usage: setCollider("rectangle") or setCollider("rectangle", offsetX, offsetY, width, height)'); } else if (type === 'rectangle' && arguments.length > 5) { pInst._warn('Extra parameters to setCollider were ignored. Usage: setCollider("rectangle") or setCollider("rectangle", offsetX, offsetY, width, height)'); } this.colliderType = 'custom'; var v = createVector(offsetX, offsetY); if (type === 'rectangle' && arguments.length === 1) { this.collider = new AABB(pInst, this.position, createVector(this.width, this.height)); } else if (type === 'rectangle' && arguments.length >= 5) { this.collider = new AABB(pInst, this.position, createVector(width, height), v); } else if (type === 'circle' && arguments.length === 1) { this.collider = new CircleCollider(pInst, this.position, Math.floor(Math.max(this.width, this.height) / 2)); } else if (type === 'circle' && arguments.length >= 4) { this.collider = new CircleCollider(pInst, this.position, width, v); } quadTree.insert(this); }; /** * Returns a the bounding box of the current image * @method getBoundingBox */ this.getBoundingBox = function() { var w = animations[currentAnimation].getWidth()*abs(this._getScaleX()); var h = animations[currentAnimation].getHeight()*abs(this._getScaleY()); //if the bounding box is 1x1 the image is not loaded //potential issue with actual 1x1 images if(w === 1 && h === 1) { //not loaded yet return new AABB(pInst, this.position, createVector(w, h)); } else { return new AABB(pInst, this.position, createVector(w, h)); } }; /** * Sets the sprite's horizontal mirroring. * If 1 the images displayed normally * If -1 the images are flipped horizontally * If no argument returns the current x mirroring * * @method mirrorX * @param {Number} dir Either 1 or -1 * @return {Number} Current mirroring if no parameter is specified */ this.mirrorX = function(dir) { if(dir === 1 || dir === -1) dirX = dir; else return dirX; }; /** * Sets the sprite's vertical mirroring. * If 1 the images displayed normally * If -1 the images are flipped vertically * If no argument returns the current y mirroring * * @method mirrorY * @param {Number} dir Either 1 or -1 * @return {Number} Current mirroring if no parameter is specified */ this.mirrorY = function(dir) { if(dir === 1 || dir === -1) dirY = dir; else return dirY; }; /* * Returns the value the sprite should be scaled in the X direction. * Used to calculate rendering and collisions. * @private */ this._getScaleX = function() { return this.scale; }; /* * Returns the value the sprite should be scaled in the Y direction. * Used to calculate rendering and collisions. * @private */ this._getScaleY = function() { return this.scale; }; /** * Manages the positioning, scale and rotation of the sprite * Called automatically, it should not be overridden * @private * @final * @method display */ this.display = function() { if (this.visible && !this.removed) { push(); colorMode(RGB); noStroke(); rectMode(CENTER); ellipseMode(CENTER); imageMode(CENTER); translate(this.position.x, this.position.y); scale(this._getScaleX()*dirX, this._getScaleY()*dirY); if (pInst._angleMode === pInst.RADIANS) { rotate(radians(this.rotation)); } else { rotate(this.rotation); } this.draw(); //draw debug info pop(); if(this.debug) { push(); //draw the anchor point stroke(0, 255, 0); strokeWeight(1); line(this.position.x-10, this.position.y, this.position.x+10, this.position.y); line(this.position.x, this.position.y-10, this.position.x, this.position.y+10); noFill(); //depth number noStroke(); fill(0, 255, 0); textAlign(LEFT, BOTTOM); textSize(16); text(this.depth+'', this.position.x+4, this.position.y-2); noFill(); stroke(0, 255, 0); //bounding box if(this.collider !== undefined) { this.collider.draw(); } pop(); } } }; /** * Manages the visuals of the sprite. * It can be overridden with a custom drawing function. * The 0,0 point will be the center of the sprite. * Example: * sprite.draw = function() { ellipse(0,0,10,10) } * Will display the sprite as circle. * * @method draw */ this.draw = function() { if(currentAnimation !== '' && animations) { if(animations[currentAnimation]) animations[currentAnimation].draw(0, 0, 0); } else { noStroke(); fill(this.shapeColor); rect(0, 0, this._internalWidth, this._internalHeight); } }; /** * Removes the Sprite from the sketch. * The removed Sprite won't be drawn or updated anymore. * * @method remove */ this.remove = function() { this.removed = true; quadTree.removeObject(this); //when removed from the "scene" also remove all the references in all the groups while (this.groups.length > 0) { this.groups[0].remove(this); } }; /** * Sets the velocity vector. * * @method setVelocity * @param {Number} x X component * @param {Number} y Y component */ this.setVelocity = function(x, y) { this.velocity.x = x; this.velocity.y = y; }; /** * Calculates the scalar speed. * * @method getSpeed * @return {Number} Scalar speed */ this.getSpeed = function() { return this.velocity.mag(); }; /** * Calculates the movement's direction in degrees. * * @method getDirection * @return {Number} Angle in degrees */ this.getDirection = function() { var direction = atan2(this.velocity.y, this.velocity.x); if(isNaN(direction)) direction = 0; // Unlike Math.atan2, the atan2 method above will return degrees if // the current p5 angleMode is DEGREES, and radians if the p5 angleMode is // RADIANS. This method should always return degrees (for now). // See https://github.com/molleindustria/p5.play/issues/94 if (pInst._angleMode === pInst.RADIANS) { direction = degrees(direction); } return direction; }; /** * Adds the sprite to an existing group * * @method addToGroup * @param {Object} group */ this.addToGroup = function(group) { if(group instanceof Array) group.add(this); else print('addToGroup error: '+group+' is not a group'); }; /** * Limits the scalar speed. * * @method limitSpeed * @param {Number} max Max speed: positive number */ this.limitSpeed = function(max) { //update linear speed var speed = this.getSpeed(); if(abs(speed)>max) { //find reduction factor var k = max/abs(speed); this.velocity.x *= k; this.velocity.y *= k; } }; /** * Set the speed and direction of the sprite. * The action overwrites the current velocity. * If direction is not supplied, the current direction is maintained. * If direction is not supplied and there is no current velocity, the current * rotation angle used for the direction. * * @method setSpeed * @param {Number} speed Scalar speed * @param {Number} [angle] Direction in degrees */ this.setSpeed = function(speed, angle) { var a; if (typeof angle === 'undefined') { if (this.velocity.x !== 0 || this.velocity.y !== 0) { a = pInst.atan2(this.velocity.y, this.velocity.x); } else { if (pInst._angleMode === pInst.RADIANS) { a = radians(this._rotation); } else { a = this._rotation; } } } else { if (pInst._angleMode === pInst.RADIANS) { a = radians(angle); } else { a = angle; } } this.velocity.x = cos(a)*speed; this.velocity.y = sin(a)*speed; }; /** * Pushes the sprite in a direction defined by an angle. * The force is added to the current velocity. * * @method addSpeed * @param {Number} speed Scalar speed to add * @param {Number} angle Direction in degrees */ this.addSpeed = function(speed, angle) { var a; if (pInst._angleMode === pInst.RADIANS) { a = radians(angle); } else { a = angle; } this.velocity.x += cos(a) * speed; this.velocity.y += sin(a) * speed; }; /** * Pushes the sprite toward a point. * The force is added to the current velocity. * * @method attractionPoint * @param {Number} magnitude Scalar speed to add * @param {Number} pointX Direction x coordinate * @param {Number} pointY Direction y coordinate */ this.attractionPoint = function(magnitude, pointX, pointY) { var angle = atan2(pointY-this.position.y, pointX-this.position.x); this.velocity.x += cos(angle) * magnitude; this.velocity.y += sin(angle) * magnitude; }; /** * Adds an image to the sprite. * An image will be considered a one-frame animation. * The image should be preloaded in the preload() function using p5 loadImage. * Animations require a identifying label (string) to change them. * The image is stored in the sprite but not necessarily displayed * until Sprite.changeAnimation(label) is called * * Usages: * - sprite.addImage(label, image); * - sprite.addImage(image); * * If only an image is passed no label is specified * * @method addImage * @param {String|p5.Image} label Label or image * @param {p5.Image} [img] Image */ this.addImage = function() { if(typeof arguments[0] === 'string' && arguments[1] instanceof p5.Image) this.addAnimation(arguments[0], arguments[1]); else if(arguments[0] instanceof p5.Image) this.addAnimation('normal', arguments[0]); else throw('addImage error: allowed usages are or